<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="description" content="妄想山海-混沌风景点地图">
  <meta name="keywords" content="妄想山海,妄想山海混沌风景点,妄想山海混沌地图,妄想山海混沌风景点">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>妄想山海-混沌地图</title>
  <link rel="shortcut icon" type="text/css" href="favicon.ico"/>
  <!-- 引入样式文件 -->
  <link rel="stylesheet" href="./static/lib/element-plus/index.css">
  <link rel="stylesheet" href="./static/lib/ol/ol.css"/>

  <link rel="stylesheet" href="./static/index.css"/>

  <!-- 引入 Vue 和 Vant 的 JS 文件 -->
  <script src="./static/lib/vue/vue.global.prod.js"></script>
  <script src="./static/lib/element-plus/index.full.min.js"></script>
  <script src="./static/lib/element-plus/icons-vue/index.iife.min.js"></script>
  <script src="./static/lib/ol/ol.js"></script>

  <script>
  var _hmt = _hmt || [];
  (function () {
    var hm = document.createElement("script");
    hm.src = "https://hm.baidu.com/hm.js?b80ae031c154d6dc96f4a9e585c07d0b";
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(hm, s);
  })();
  </script>
</head>
<body>
<div id="app" style="display: flex;flex-direction: column;">
  <div style="display: flex;padding: 10px;">
    <el-select
      v-model="topSearch"
      placeholder="请输入要查询的风景点名称"
      clearable
      filterable
      multiple
      collapse-tags
      :max-collapse-tags="isMobile ? 10 : 20"
      filterable
      no-match-text="未查询到匹配数据"
      no-data-text="未查询到匹配数据"
      @clear="location()"
      @remove-tag="location(topSearch,true)"
    >
      <template #header>
        <el-checkbox-group v-model="form.checkList">
          <el-checkbox-button v-for="(op,idx) in selectOptions" :class="{['fl'+op.label]:colorful,colorful}" :key="idx"
                              :value="op.label">
            {{ op.label }}({{ op.options?.length || 0 }})
          </el-checkbox-button>
        </el-checkbox-group>
      </template>
      <template
        v-for="group in selectOptions"
        :key="group.label"
      >
        <el-option-group
          v-show="form.checkList.includes(group.label)"
          :label="group.label"
          :class="{['fl'+group.label]:colorful}"
        >
          <el-option
            v-for="item in group.options"
            :key="item.uid"
            :label="item.name"
            :value="item.uid"
          >
            <span style="float: left">{{ item.name }}</span>
            <span style="float: right;color: var(--el-text-color-secondary);font-size: 13px;">
            {{ item.qypname }} - {{ item.qyname }}
          </span>
          </el-option>
        </el-option-group>
      </template>
    </el-select>
    <el-button type="primary" style="margin-left: 8px;" :icon="Search" @click="location(topSearch)">查找</el-button>
    <el-button type="info" @click="drawerVisible = true">批量导入</el-button>
  </div>

  <div id="map-box" class="ol-unselectable" style="flex: 1;position:relative;">
    <div style="display: flex;padding: 0 10px 10px;position:absolute;z-index: 1;">
      <el-switch
        v-model="colorful"
        size="large"
        inline-prompt
        active-text="多色显示"
        inactive-text="单色显示">
      </el-switch>
    </div>
  </div>

  <el-drawer v-model="drawerVisible" append-to-body direction="btt" size="70%">
    <el-form :model="form" label-width="auto">
      <el-form-item label="批量提取：">
        <el-input
          v-model="form.desc"
          type="textarea"
          :placeholder="'请输入风景点名称,每个一行或以空格分割\n如：\n倚风崖\n临渊壁\n浑天谷\n鏖战野 凭高峰 峥嵘谷 九曲湾\n鏖战 高峰 峥嵘 九曲'"
          :autosize="{ minRows: 4, maxRows: 10 }"
          clearable
        >
        </el-input>
        <div style="margin-top: 8px;">
          <el-button type="primary" @click="form.desc =''">清空</el-button>
          <el-button type="primary" @click="extract">提取</el-button>
        </div>
      </el-form-item>
      <el-divider></el-divider>
      <el-form-item label="百度识图">
        <el-input
          v-model="form.result"
          placeholder="请粘贴百度文字识别JSON结果"
          :formatter="(value) => value.replace(/[\s\t]/g, '')"
          clearable
        >
        </el-input>
        <div style="margin-top: 8px;">
          <el-button type="info" @click="baiduDialogVisible=true">说明</el-button>
          <el-button type="info" tag="a" href="https://ai.baidu.com/tech/ocr/general" target="_blank">
            前往百度文字识别
          </el-button>
          <el-button type="primary" @click="baiduExtract">提取</el-button>
        </div>
      </el-form-item>
      <el-divider></el-divider>
      <el-form-item label=" ">
        <div>
          <!-- 1:732875 96-->
          <el-link href="./static/img/鹰眼图-无标记.png" download="鹰眼图-无标记.png">
            下载《鹰眼图-无标记.png》（387KB）
          </el-link>
          <el-link href="./static/img/鹰眼图-无标记-多色.png" download="鹰眼图-无标记-多色.png">
            下载《鹰眼图-无标记-多色.png》（372KB）
          </el-link>

          <!-- 1:366438 96-->
          <el-link href="./static/img/单色带标记.png" download="单色带标记.png">
            下载《单色带标记.png》（1.21MB）
          </el-link>
          <el-link href="./static/img/多色带标记.png" download="多色带标记.png">
            下载《多色带标记.png》（1.29MB）
          </el-link>
        </div>
      </el-form-item>
    </el-form>
  </el-drawer>

  <el-dialog v-model="baiduDialogVisible" :show-close="false" fullscreen append-to-body>
    <template #header="{ close, titleId, titleClass }">
      <div style="display: flex;flex-direction: row;justify-content: space-between;">
        <h4 :id="titleId" :class="titleClass">百度文字识别步骤说明</h4>
        <div style="padding-right: 30px;">
          <el-affix :offset="20">
            <el-button :icon="CloseBold" type="danger" circle @click="close"/>
          </el-affix>
        </div>
      </div>
    </template>
    <el-steps direction="vertical">
      <el-step title="访问 百度AI开放平台" status="process">
        <template #description>
          <div>
            <el-link target="_blank" href="https://ai.baidu.com/tech/ocr/general" download="多色带标记.png">
              https://ai.baidu.com/tech/ocr/general
            </el-link>
          </div>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[0]"
            fit="contain"
            :preview-src-list="none"
            :initial-index="0"
            :infinite="false"
          >
          </el-image>
        </template>
      </el-step>
      <el-step title="登录百度账号" status="process">
        <template #description>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[1]"
            fit="contain"
            :preview-src-list="baiduAiImg"
            :initial-index="1"
            :infinite="false"
          >
          </el-image>
        </template>
      </el-step>
      <el-step title="识别风景点图片，复制识别结果" status="process">
        <template #description>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[2]"
            fit="contain"
            :preview-src-list="baiduAiImg"
            :initial-index="2"
            :infinite="false"
          >
          </el-image>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[3]"
            fit="contain"
            :preview-src-list="baiduAiImg"
            :initial-index="3"
            :infinite="false"
          >
          </el-image>
        </template>
      </el-step>
      <el-step title="回到查询系统，粘贴结果" status="process">
        <template #description>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[4]"
            fit="contain"
            :preview-src-list="baiduAiImg"
            :initial-index="4"
            :infinite="false"
          >
          </el-image>
        </template>
      </el-step>
    </el-steps>
    <el-divider></el-divider>
    <el-steps direction="vertical">
      <el-step title="后话" status="success" :icon="Coin">
        <template #description>
          <el-image
            :style="{ width: isMobile ? '100%' : '50%' }"
            style="border: 1px solid #e3e3e3;vertical-align: top;"
            :src="baiduAiImg[5]"
            fit="contain"
            :preview-src-list="[baiduAiImg[5]]"
          >
          </el-image>
        </template>
      </el-step>
    </el-steps>
    <template #footer2>
      <div class="dialog-footer">
        <el-button @click="centerDialogVisible = false">Cancel</el-button>
        <el-button type="primary" @click="centerDialogVisible = false">
          Confirm
        </el-button>
      </div>
    </template>
  </el-dialog>
</div>

<script>
const {createApp, ref, onMounted, watch, computed} = Vue
const {ElMessage, ElMessageBox} = ElementPlus
const {Search} = ElementPlusIconsVue
const cm = {
  '灵智': '#e61515',
  '灵能': '#e66215',
  '灵智之核': '#e6ae15',
  '无礼包': '#1596e6',
}

const App = {
  setup() {
    const topSearch = ref([])
    const selectOptions = ref([])
    const drawerVisible = ref(false)
    const colorful = ref(true)
    const girdHighlightUid = ref(null)
    const isMobile = isMobileDevice()
    const baiduDialogVisible = ref(false)
    const form = ref({
      desc: '',
      result: ''
    })

    const baiduAiImg = [
      './static/img/baiduai/home.png',
      './static/img/baiduai/login.png',
      './static/img/baiduai/upload.png',
      './static/img/baiduai/demo-fjd.png',
      './static/img/baiduai/res.png',
      './static/img/baiduai/qiong.png',
    ]

    const view = new ol.View({
      enableRotation: false,
      constrainOnlyCenter: true,
      center: [0, 0],
      zoom: 11,
      minZoom: 10,
      maxZoom: 14,
      extent: [25, -28775, 32025, 3225],
      padding: [20, 20, 20, 20]
    })
    const mapInstance = new ol.Map({
      view,
      controls: [],
      interactions: ol.interaction.defaults.defaults({
        // altShiftDragRotate: false,
        // pinchRotate: false,
        zoomDelta: true
      })
    })

    mapInstance.on('pointermove', function (evt) {
      mapInstance.getTargetElement().style.cursor = mapInstance.hasFeatureAtPixel(evt.pixel, {
        layerFilter: l => l === pointVectorLayer,
        hitTolerance: 30
      }) ? 'pointer' : '';
    });

    const areaVectorLayer = new ol.layer.Vector({
      source: new ol.source.Vector({
        url: './static/geojson/区域范围.geojson',
        format: new ol.format.GeoJSON(),
      }),
      style: (f) => {
        const showText = view.getZoom() < 13
        return new ol.style.Style({
          stroke: new ol.style.Stroke({
            color: '#ffffff',
            width: 1,
          }),
          fill: new ol.style.Fill({
            color: `rgb(${f.get('fillColor')})`
          }),
          text: new ol.style.Text({
            font: '16px sans-serif',
            text: showText ? f.get('name') : '',
            fill: new ol.style.Fill({
              color: [255, 255, 255, 1],
            }),
            stroke: new ol.style.Stroke({
              color: [168, 50, 153, 0.6],
              width: 4,
            }),
            padding: [2, 2, 2, 2],
          }),
        })
      },
      visible: true,
      zIndex: 1,
      map: mapInstance
    });
    window.mapInstance = mapInstance

    areaVectorLayer.getSource().once('featuresloadend', () => {
      let extent = areaVectorLayer.getSource().getExtent()
      view.fit(extent, {})
    })

    const specialAreaVectorLayer = new ol.layer.Vector({
      source: new ol.source.Vector({
        url: './static/geojson/特殊点位区域范围.geojson',
        format: new ol.format.GeoJSON(),
      }),
      style: (f) => {
        let showText = view.getZoom() > 12
        return new ol.style.Style({
          fill: new ol.style.Fill({
            color: `rgb(${f.get('fillColor')})`
          }),
          text: showText ? new ol.style.Text({
            font: '12px sans-serif',
            text: f.get('name'),
            overflow: true,
            fill: new ol.style.Fill({
              color: [255, 255, 255, 1],
            }),
            stroke: new ol.style.Stroke({
              color: [168, 50, 153, 0.6],
              width: 4,
            }),
            padding: [2, 2, 2, 2],
          }) : null,
        })
      },
      visible: true,
      zIndex: 1,
      minZoom: 12.5,
      map: mapInstance
    });

    const girdVectorLayer = new ol.layer.Vector({
      source: new ol.source.Vector({
        url: './static/geojson/网格.geojson',
        format: new ol.format.GeoJSON(),
      }),
      style: (f) => {
        let color = '#e3e3e388'

        if (girdHighlightUid.value) {
          let extent = pointVectorSource.getFeatureById(girdHighlightUid.value).getGeometry().getExtent()
          let some = f.getGeometry().intersectsExtent(extent)
          if (some) {
            color = '#e61515'
          }
        }
        return new ol.style.Style({
          stroke: new ol.style.Stroke({
            color,
            width: 1,
          })
        })
      },
      visible: true,
      zIndex: 2,
      map: mapInstance
    });

    const pointVectorSource = new ol.source.Vector({
      url: './static/geojson/风景点.geojson',
      format: new ol.format.GeoJSON(),
    })
    const pointVectorLayer = new ol.layer.Vector({
      source: pointVectorSource,
      style: (f) => {
        let showText = view.getZoom() > 12
        let textStrokeColor = '#e3e3e3'
        if (girdHighlightUid.value === f.getId()) {
          textStrokeColor = '#4feaa2'
        }

        return new ol.style.Style({
          image: new ol.style.Circle({
            fill: new ol.style.Fill({
              color: colorful.value ? cm[f.get('lb')] : '#e61515',
            }),
            stroke: new ol.style.Stroke({
              color: '#e3e3e388',
              width: 1,
            }),
            radius: 3,
          }),
          text: showText ? new ol.style.Text({
            font: '14px sans-serif',
            text: f.get('name'),
            fill: new ol.style.Fill({
              color: colorful.value ? cm[f.get('lb')] : '#ff0000',
            }),
            stroke: new ol.style.Stroke({
              color: textStrokeColor,
              width: 4,
            }),
            padding: [2, 2, 1, 2],
            offsetY: -14,
          }) : null,
        })
      },
      visible: true,
      zIndex: 3,
      map: mapInstance
    });

    mapInstance.on('click', (f) => {
      let featureAtPixel = mapInstance.getFeaturesAtPixel(f.pixel, {
        layerFilter: l => l === pointVectorLayer
      })[0]
      if (featureAtPixel) {
        let uid = featureAtPixel.getId()
        girdHighlightUid.value = uid
        highlightByUid(uid)
      }
    })

    const pointFeatures = []
    const pointProperties = []

    pointVectorSource.once('featuresloadend', () => {
      pointFeatures.push(...pointVectorSource.getFeatures())
      pointProperties.push(...pointFeatures.map((f, idx) => {
        f.setId(idx + 1)
        f.set('lb', f.get('lb') || '无礼包')

        let {geometry, ...attrs} = f.getProperties()
        return {
          ...attrs,
          uid: f.getId()
        }
      }))

      let res = groupBy(pointProperties, 'lb')
      let map = Object.entries(res).map(([k, v]) => ({
        label: k,
        options: v,
      }))
      selectOptions.value = map
      form.value.checkList = map.map(i => i.label)
    })

    onMounted(() => {
      mapInstance.setTarget('map-box')
    })

    function groupBy(arr = [], by) {
      const res = {}
      arr.forEach(i => {
        let key = i[by]
        if (!(key in res)) {
          res[key] = []
        }
        res[key].push(i)
      })
      return res
    }

    function location(ids = []) {
      let features

      let firstId = ids[0]
      if (ids.length && firstId) {
        features = pointFeatures.filter(f => ids.includes(f.getId()))
      } else {
        const lbs = form.value.checkList
        features = pointFeatures.filter(f => lbs.includes(f.get('lb')))
      }

      pointVectorSource.clear()
      pointVectorSource.addFeatures(features)

      girdHighlightUid.value = firstId

      if (firstId) {
        highlightByUid(firstId)
      } else {
        view.fit(pointVectorSource.getExtent(), {
          duration: 300
        })
      }
    }

    async function extract() {
      let desc = form.value.desc
      desc = desc.replaceAll(/\n| +/g, ' ')
      if (!desc.length) {
        ElMessage({
          message: '请先输入内容',
          grouping: true,
          type: 'warning',
        })
        return
      }
      const arr = desc.split(' ')

      let filter = pointProperties.filter(p => arr.some(i => p.name.includes(i)))
      filter = filter.sort((a, b) => (arr.indexOf(a.name) - arr.indexOf(b.name)))

      if (topSearch.value?.length) {
        await ElMessageBox.confirm(
          '多选风景点已经存在值，是否覆盖?',
          '提示',
          {
            distinguishCancelAndClose: true,
            confirmButtonText: '覆盖',
            cancelButtonText: '取消',
          }
        )
      }
      topSearch.value = filter.map(i => i.uid)
      location(topSearch.value)
      drawerVisible.value = false
    }

    async function baiduExtract() {
      let desc = form.value.result

      if (!desc.length) {
        ElMessage({
          message: '请先粘贴内容',
          grouping: true,
          type: 'warning',
        })
        return
      }

      try {
        desc = JSON.parse(desc)
      } catch (e) {
        ElMessage({
          message: '粘贴内容有误，请注意前后的括号',
          grouping: true,
          type: 'warning',
        })
        return
      }

      const arr = desc.words_result.map(i => i.words)

      let filter = pointProperties.filter(p => arr.some(i => p.name.includes(i)))
      filter = filter.sort((a, b) => (arr.indexOf(a.name) - arr.indexOf(b.name)))

      if (topSearch.value?.length) {
        await ElMessageBox.confirm(
          '多选风景点已经存在值，是否覆盖?',
          '提示',
          {
            distinguishCancelAndClose: true,
            confirmButtonText: '覆盖',
            cancelButtonText: '取消',
          }
        )
      }
      topSearch.value = filter.map(i => i.uid)
      location(topSearch.value)
      drawerVisible.value = false
    }

    function highlightByUid(uid) {
      const featureById = pointVectorLayer.getSource().getFeatureById(uid)
      const extent = featureById.getGeometry().getExtent()
      view.fit(extent, {
        duration: 300
      })
    }

    watch(() => form.value.checkList, () => {
      const lbs = form.value.checkList
      const topSearchs = topSearch.value
      const filter = pointFeatures.filter(f => lbs.includes(f.get('lb')) || topSearchs.includes(f.getId()))

      pointVectorSource.clear()
      pointVectorSource.addFeatures(filter)
    })

    watch(colorful, () => {
      pointVectorLayer.setSource(null)
      setTimeout(() => {
        pointVectorLayer.setSource(pointVectorSource)
      }, 0)
    })

    return {
      isMobile,
      topSearch,
      selectOptions,
      drawerVisible,
      colorful,
      form,
      baiduDialogVisible,
      baiduAiImg,
      Search: ElementPlusIconsVue.Search,
      Coin: ElementPlusIconsVue.Coin,
      CloseBold: ElementPlusIconsVue.CloseBold,
      location,
      extract,
      baiduExtract
    }
  }
}

function isMobileDevice() {
  let userAgent = navigator.userAgent || navigator.vendor || window.opera;
  return /android|avantgo|blackberry|iemobile|ipad|iphone|ipod|opera mini|opera mobi|palm|pocket|psp|series([46])0|symbian|windows ce|windows phone|xda|xiino/i.test(userAgent)
}

const app = createApp(App)
app.use(ElementPlus, {size: isMobileDevice() ? 'small' : 'default'});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.mount('#app');
</script>
</body>
</html>
